﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Web;

namespace Cryomancer.StarChart
{
	/// <summary>
	/// Contains the data needed to perform a VSP request
	/// </summary>
	[Serializable]
	public class VSPChartDetail : ICloneable, INotifyPropertyChanged
	{
		#region Constants
		private const string RequestString = @"name={0}&chartid={12}&ra={1}&dec={2}&charttitle={3}&chartcomment={4}&aavsoscale={5}&fov={6}&maglimit={8}&resolution={7}&north={9}&east={10}&dss=no&ccdtable=no&othervars={11}&form_build_id=form-13078e03b417169fc07e63e4d361545c&form_id=vsp_form&submit.x=13&submit.y=11";
		private const string KeyString     = @"{0}.{1}.{2}.{3}.{4}.{5}.{6}.png";
		#endregion

		#region Members
		private string             name;
		private string             ra;
		private string             dec;
		private string             title;
		private string             comment;
		private ChartCode          chartCode;
		private double             fov;
		private int                resolution;
		private double             magLimit;
		private NorthOrientation   north;
		private EastOrientation    east;
		private MarkOtherVariables otherVars;
		private string             chartId;

		[NonSerialized]
		private Image chartImage;

		#endregion

		#region Properties

		/// <summary>
		/// Gets or sets the name
		/// </summary>
		public string Name
		{
			get
			{
				return this.name;
			}
			set
			{
				if (this.name == value)
				{
					return;
				}

				this.name = value;
				OnPropertyChanged("Name");
			}
		}

		/// <summary>
		/// Gets or sets the right ascension.
		/// </summary>
		public string RA
		{
			get
			{
				return this.ra;
			}
			set
			{
				if (this.ra == value)
				{
					return;
				}

				this.ra = value;
				OnPropertyChanged("RA");
			}
		}

		/// <summary>
		/// Gets or sets the declination.
		/// </summary>
		public string Declination
		{
			get
			{
				return this.dec;
			}
			set
			{
				if (this.dec == value)
				{
					return;
				}

				this.dec = value;
				OnPropertyChanged("Declination");
			}
		}

		/// <summary>
		/// Gets or sets the title
		/// </summary>
		public string Title
		{
			get
			{
				return this.title;
			}
			set
			{
				if (this.title == value)
				{
					return;
				}

				this.title = value;
				OnPropertyChanged("Title");
			}
		}

		/// <summary>
		/// Gets or sets the comment.
		/// </summary>
		public string Comment
		{
			get
			{
				return this.comment;
			}
			set
			{
				if (this.comment == value)
				{
					return;
				}

				this.comment = value;
				OnPropertyChanged("Comment");
			}
		}

		/// <summary>
		/// Gets or sets the ChartCode, if any.  Setting this value will change the
		/// settings in many of the other fields.
		/// </summary>
		public ChartCode LegacyChartCode
		{
			get
			{
				return this.chartCode;
			}
			set
			{
				SetChartCode(value);
			}
		}

		/// <summary>
		/// Gets or sets the field of view
		/// </summary>
		public double FOV
		{
			get
			{
				return this.fov;
			}
			set
			{
				SetFOV(value);
			}
		}

		/// <summary>
		/// Gets or sets the resolution
		/// </summary>
		public int Resolution
		{
			get
			{
				return this.resolution;
			}
			set
			{
				SetResolution(value);
			}
		}

		/// <summary>
		/// Gets or sets the mag limit
		/// </summary>
		public double MagLimit
		{
			get
			{
				return this.magLimit;
			}
			set
			{
				SetMagLimit(value);
			}
		}

		/// <summary>
		/// Gets or sets the North orientation
		/// </summary>
		public NorthOrientation North
		{
			get
			{
				return this.north;
			}
			set
			{
				SetNorth(value);
			}
		}

		/// <summary>
		/// Gets or sets the East orientation
		/// </summary>
		public EastOrientation East
		{
			get
			{
				return this.east;
			}
			set
			{
				SetEast(value);
			}
		}

		/// <summary>
		/// Gets or sets whether to return None, Some, or All other variables on the chart.
		/// </summary>
		public MarkOtherVariables OtherVariables
		{
			get
			{
				return this.otherVars;
			}
			set
			{
				if (this.otherVars == value)
				{
					return;
				}

				this.otherVars = value;
				OnPropertyChanged("OtherVariables");
			}
		}

		/// <summary>
		/// Gets or sets the chart ID.
		/// </summary>
		public string ChartID
		{
			get
			{
				return this.chartId;
			}
			set
			{
				if (this.chartId == value)
				{
					return;
				}

				this.chartId = value;
				OnPropertyChanged("ChartID");
			}
		}

		/// <summary>
		/// Gets or sets the chart image.  Can be null.
		/// </summary>
		public Image ChartImage
		{
			get
			{
				return this.chartImage;
			}
			set
			{
				this.chartImage = value;
			}
		}

		#endregion

		#region C'Tors and object lifetime code
		public VSPChartDetail()
		{
			this.name       = String.Empty;
			this.ra         = String.Empty;
			this.dec        = String.Empty;
			this.title      = String.Empty;
			this.comment    = String.Empty;
			this.chartCode  = ChartCode.choose;
			this.resolution = 75;
			this.magLimit   = 5.0d;
			this.north      = NorthOrientation.Up;
			this.east       = EastOrientation.Left;
			this.otherVars  = MarkOtherVariables.None;
			this.chartId    = String.Empty;
		}
		#endregion

		#region Methods
		/// <summary>
		/// Gets a HTTPEncoded url string appropriate for a VSP "Get" request.
		/// </summary>
		/// <returns>A correctly formatted request.</returns>
		public string ToUrlString()
		{
			string rawOtherVars = String.Empty;
			switch (this.otherVars)
			{
				case MarkOtherVariables.All:
				case MarkOtherVariables.GCVS:
					rawOtherVars = Enum.GetName(typeof(MarkOtherVariables), this.otherVars).ToLower();
					break;
			}

			string retVal = String.Format(RequestString,
				HttpUtility.UrlEncode(this.name),
				HttpUtility.UrlEncode(this.ra),
				HttpUtility.UrlEncode(this.dec),
				HttpUtility.UrlEncode(this.title),
				HttpUtility.UrlEncode(this.comment),
				Enum.GetName(typeof(ChartCode), this.chartCode),
				Math.Round(this.fov, 1),
				this.resolution,
				Math.Round(this.magLimit, 1),
				Enum.GetName(typeof(NorthOrientation), this.north).ToLower(),
				Enum.GetName(typeof(EastOrientation),  this.east).ToLower(),
				rawOtherVars,
				HttpUtility.UrlEncode(this.chartId));

			return retVal;
		}

		public string ToRequestString()
		{
			string rawOtherVars = String.Empty;
			switch (this.otherVars)
			{
				case MarkOtherVariables.All:
				case MarkOtherVariables.GCVS:
					rawOtherVars = Enum.GetName(typeof(MarkOtherVariables), this.otherVars).ToLower();
					break;
			}

			string retVal = String.Format(RequestString,
				this.name,
				this.ra,
				this.dec,
				this.title,
				this.comment,
				Enum.GetName(typeof(ChartCode), this.chartCode),
				Math.Round(this.fov, 1),
				this.resolution,
				Math.Round(this.magLimit, 1),
				Enum.GetName(typeof(NorthOrientation), this.north).ToLower(),
				Enum.GetName(typeof(EastOrientation),  this.east).ToLower(),
				rawOtherVars,
				this.chartId);

			return retVal;
		}

		/// <summary>
		/// Gets a "unique" key that can be used to save the image in the file system.
		/// </summary>
		/// <returns>A formatted string.</returns>
		public string ToKeyString()
		{
			string rawOtherVars = String.Empty;
			switch (this.otherVars)
			{
				case MarkOtherVariables.All:
				case MarkOtherVariables.GCVS:
					rawOtherVars = Enum.GetName(typeof(MarkOtherVariables), this.otherVars).ToLower();
					break;
			}

			string retVal = String.Format(KeyString,
				CleanMemberVal(this.name),
				Math.Round(this.fov, 1),
				this.resolution,
				Math.Round(this.magLimit, 1),
				Enum.GetName(typeof(NorthOrientation), this.north).ToLower(),
				Enum.GetName(typeof(EastOrientation),  this.east).ToLower(),
				rawOtherVars);

			return retVal;
		}

		/// <summary>
		/// Returns a deep-copy clone of the instance, without the ChartImage
		/// </summary>
		/// <returns>A deep-copy clone.</returns>
		public VSPChartDetail GetDuplicate()
		{
			VSPChartDetail dup = Clone() as VSPChartDetail;
			dup.chartImage = null;

			return dup;
		}

		/// <summary>
		/// Sets the contents of this detail object to the values of another.
		/// </summary>
		/// <param name="detail">A valid detail object</param>
		public void SetDetail(VSPChartDetail detail)
		{
			if (detail == null)
			{
				throw new ArgumentNullException("detail");
			}

			if (this == detail)
			{
				return;
			}

			this.Name            = detail.Name;
			this.RA              = detail.RA;
			this.Declination     = detail.Declination;
			this.Title           = detail.Title;
			this.Comment         = detail.Comment;
			this.LegacyChartCode = detail.LegacyChartCode;
			this.FOV             = detail.FOV;
			this.Resolution      = detail.Resolution;
			this.MagLimit        = detail.MagLimit;
			this.North           = detail.North;
			this.East            = detail.East;
			this.OtherVariables  = detail.OtherVariables;
			this.ChartID         = detail.ChartID;
			this.ChartImage      = detail.ChartImage;
		}

		private void SetChartCode(ChartCode code)
		{
			if (this.chartCode == code)
			{
				return;
			}

			this.chartCode = code;
			OnPropertyChanged("ChartCode");

			switch (code)
			{
				case ChartCode.choose:
					break;

				case ChartCode.A:
					this.FOV        = 900;
					this.MagLimit   = 9;
					this.North      = NorthOrientation.Down;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.AR:
					this.FOV        = 900;
					this.MagLimit   = 9;
					this.North      = NorthOrientation.Up;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.B:
					this.FOV        = 180;
					this.MagLimit   = 11;
					this.North      = NorthOrientation.Down;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.BR:
					this.FOV        = 180;
					this.MagLimit   = 11;
					this.North      = NorthOrientation.Up;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.C:
					this.FOV        = 120;
					this.MagLimit   = 12;
					this.North      = NorthOrientation.Down;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.CR:
					this.FOV        = 120;
					this.MagLimit   = 12;
					this.North      = NorthOrientation.Up;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.D:
					this.FOV        = 60;
					this.MagLimit   = 14.5;
					this.North      = NorthOrientation.Down;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.DR:
					this.FOV        = 60;
					this.MagLimit   = 14.5;
					this.North      = NorthOrientation.Up;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.E:
					this.FOV        = 30;
					this.MagLimit   = 16.5;
					this.North      = NorthOrientation.Down;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.ER:
					this.FOV        = 30;
					this.MagLimit   = 16.5;
					this.North      = NorthOrientation.Up;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.F:
					this.FOV        = 18.5;
					this.MagLimit   = 18.5;
					this.North      = NorthOrientation.Down;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.FR:
					this.FOV        = 18.5;
					this.MagLimit   = 18.5;
					this.North      = NorthOrientation.Up;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.G:
					this.FOV        = 7.5;
					this.MagLimit   = 20.5;
					this.North      = NorthOrientation.Down;
					this.East       = EastOrientation.Right;
					break;

				case ChartCode.GR:
					this.FOV        = 7.5;
					this.MagLimit   = 20.5;
					this.North      = NorthOrientation.Up;
					this.East       = EastOrientation.Right;
					break;

				default:
					throw new ArgumentException("Unknown chart code value.");
			}
		}

		private void SetFOV(double fov)
		{
			if (this.fov == fov)
			{
				return;
			}

			if (fov < 0 || fov > 900)
			{
				throw new ArgumentOutOfRangeException("fov", "FOV must be between 0 and 900");
			}

			this.fov = fov;
			OnPropertyChanged("FOV");
		}

		private void SetEast(EastOrientation value)
		{
			if (this.east == value)
			{
				return;
			}

			switch (value)
			{
				case EastOrientation.Left:   
				case EastOrientation.Right:
					this.east = value; 
					OnPropertyChanged("East");
					break;

				default:
					throw new ArgumentOutOfRangeException("value", "East can only be 'Left' or 'Right'.");
			}
		}

		private void SetNorth(NorthOrientation value)
		{
			if (this.north == value)
			{
				return;
			}

			switch (value)
			{
				case NorthOrientation.Up:
				case NorthOrientation.Down:
					this.north = value;
					OnPropertyChanged("North");
					break;

				default:
					throw new ArgumentOutOfRangeException("value", "North can only be 'Up' or 'Down'.");
			}
		}

		private void SetMagLimit(double value)
		{
			if (this.magLimit == value)
			{
				return;
			}

			if (value < 5.0d || value > 25.0d)
			{
				throw new ArgumentOutOfRangeException("value", "MagLimit must be between 5 and 25.");
			}

			this.magLimit = value;
			OnPropertyChanged("MagLimit");
		}

		private void SetResolution(int value)
		{
			if (this.resolution == value)
			{
				return;
			}

			if (value < 75 || value > 300)
			{
				throw new ArgumentOutOfRangeException("value", "Resolution must be between 75 and 300");
			}

			this.resolution = value;
			OnPropertyChanged("Resolution");
		}

		private string CleanMemberVal(string val)
		{
			if (String.IsNullOrEmpty(val))
			{
				return String.Empty;
			}

			List<char> badChars = new List<char>();
			badChars.AddRange(Path.GetInvalidFileNameChars());
			badChars.AddRange(Path.GetInvalidPathChars());
			badChars.Add(' ');

			string retVal = val.Trim().ToLower();
			foreach (char badChar in badChars)
			{
				retVal = retVal.Replace(badChar.ToString(), String.Empty);
			}

			return retVal;
		}

		#endregion

		#region ICloneable Members

		/// <summary>
		/// Creates a new object that is a copy of the current instance.
		/// </summary>
		/// <returns>A copy of the current object's instance</returns>
		public object Clone()
		{
			using (MemoryStream memStream = new MemoryStream())
			{
				BinaryFormatter bin = new BinaryFormatter();
				bin.Serialize(memStream, this);

				memStream.Seek(0, SeekOrigin.Begin);

				return bin.Deserialize(memStream);
			}
		}

		#endregion

		#region INotifyPropertyChanged Members

		[field:NonSerialized]
		public event PropertyChangedEventHandler PropertyChanged;

		protected virtual void OnPropertyChanged(string propName)
		{
			if (this.PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(propName));
			}
		}

		#endregion
	}

	#region Enumerations
	/// <summary>
	/// Legacy Chart Codes
	/// </summary>
	public enum ChartCode
	{
		choose,
		A, AR, 
		B, BR, 
		C, CR,
		D, DR,
		E, ER,
		F, FR,
		G, GR
	}

	/// <summary>
	/// Orientations for North
	/// </summary>
	public enum NorthOrientation
	{
		Up,
		Down,
	}

	/// <summary>
	/// Orientations for East
	/// </summary>
	public enum EastOrientation
	{
		Left,
		Right
	}

	/// <summary>
	/// Determines what other variables to return.
	/// </summary>
	public enum MarkOtherVariables
	{
		None,
		GCVS,
		All
	}
	#endregion
}
